#include "Model.hpp"

#include <string>
#include <iostream>
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

#include <misc/aerPath.hpp>
#include <device/aerTexture.hpp>
#include "Application.hpp"


namespace {

aer::Texture* SetupTexture( const std::string &dirname, const aiMaterial &aiMat, 
                           aiTextureType type, unsigned int index);

}



Model::Model()
{
}

Model::~Model()
{
  m_program.release();
  
  for (int i=0; i<m_objects.size(); ++i) {
    m_objects[i].vao.release();
    m_objects[i].vbo.release();
  }
}

void Model::load(const char *name)
{
  /// Init shaders ==========================
  m_program.generate();
  m_program.addShader( aer::Program::VERTEX_SHADER,   "PassThrough.Vertex" );
  m_program.addShader( aer::Program::FRAGMENT_SHADER, "PassThrough.Fragment" );
  AER_CHECK( m_program.link() );
  
  
  
  /// Load the model ========================
  Assimp::Importer importer;
  
  unsigned int flags = aiProcess_GenSmoothNormals        |
                       aiProcess_CalcTangentSpace        |   /// used for Normal Mapping
                       aiProcess_Triangulate             |   /// to have only triangles in the mesh
                       //aiProcess_JoinIdenticalVertices   |   /// to use indexed geometry
                       aiProcess_SortByPType;
  //flags = 0; // let's not use flags
  
  
  AER_ASSERT( importer.ReadFile(name, flags) != NULL );
  const aiScene* scene = importer.GetScene();
  
  
  // retrieve the model directory to get textures
  aer::Path path( name );
  const char *dirname = path.getDirectory();//
  
  
  /// A) Load meshes
  
  // Count the max num of vertex per objects
  aer::U32 maxVertices = 0;
  for (aer::U32 i=0; i<scene->mNumMeshes; ++i) {
    maxVertices = glm::max( maxVertices, scene->mMeshes[i]->mNumVertices);
  }
  
  // Allocate a temp host buffer to setup vertices
  SkinnedVertex* vertices = new SkinnedVertex[maxVertices];  
  AER_ASSERT( "Error allocation skinned vertices buffer" && vertices != NULL );
  
  
  m_objects.resize( scene->mNumMeshes );
    
  for (aer::U32 i=0; i<m_objects.size(); ++i) 
  {    
    aiMesh *aimesh = scene->mMeshes[i];  
    Object_t &obj = m_objects[i];
    
    
    obj.id_mat = aimesh->mMaterialIndex;
    obj.numVertices = aimesh->mNumVertices;
    
    /// Setup the device (GPU) buffer    
    obj.vao.generate();
    obj.vao.bind();
    
    obj.vbo.generate();
    obj.vbo.bind();
        
    /// Setup the skinned vertices on a HOST buffer
        
    const aer::U32 stride = sizeof(SkinnedVertex);
    aiVector3D attrib;
        
    if (aimesh->HasPositions()) 
    {
      for (aer::U32 j=0; j<obj.numVertices; ++j) {
        SkinnedVertex &v = vertices[j];
        attrib = aimesh->mVertices[j];
        
        v.position[0] = attrib.x;
        v.position[1] = attrib.y;
        v.position[2] = attrib.z;
      }
      
      glEnableVertexAttribArray( 0 );
      glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, stride, 
                            (const GLvoid*)(0));
    }
    
    if (aimesh->HasNormals()) 
    {
      for (aer::U32 j=0; j<obj.numVertices; ++j) {
        SkinnedVertex &v = vertices[j];
        attrib = aimesh->mNormals[j];
        
        v.normal[0] = attrib.x;
        v.normal[1] = attrib.y;
        v.normal[2] = attrib.z;
      }
      
      glEnableVertexAttribArray( 1 );
      glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, stride, 
                            (const GLvoid*)(3*sizeof(float)));
    }
    
    if (aimesh->HasTextureCoords(0)) 
    {
      for (aer::U32 j=0; j<obj.numVertices; ++j) {
        SkinnedVertex &v = vertices[j];
        attrib = aimesh->mTextureCoords[0][j];
        
        v.texCoord[0] = attrib.x;
        v.texCoord[1] = attrib.y;
      }
      
      glEnableVertexAttribArray( 2 );
      glVertexAttribPointer( 2, 2, GL_FLOAT, GL_FALSE, stride,
                            (const GLvoid*)((3+3)*sizeof(float)));
    }
        
    // TODO: same thing with bones weight data
    
    /// Upload the data to GPU
    obj.vbo.setData( vertices, sizeof(SkinnedVertex)*obj.numVertices );    
    
    obj.vao.unbind();
  }
  
  AER_SAFE_DELETEV( vertices );
  
  
  /// B) Load materials
  m_materials.resize( scene->mNumMaterials );
   
  for (aer::U32 i=0; i<scene->mNumMaterials; ++i)
  {
    const aiMaterial &aiMat = *(scene->mMaterials[i]);
    aer::Material &material = m_materials[i];
  
    aiColor3D ai_color;    
    // Get diffuse
    aiMat.Get( AI_MATKEY_COLOR_DIFFUSE, ai_color);
    material.setPhongDiffuse( aer::Vector3( ai_color.r, ai_color.g, ai_color.b) );
  
    // Get specular
    aiMat.Get( AI_MATKEY_COLOR_SPECULAR, ai_color);
    material.setPhongSpecular( aer::Vector3( ai_color.r, ai_color.g, ai_color.b) );
    
    float fvalue;        
    // Get shininess
    aiMat.Get( AI_MATKEY_SHININESS, fvalue);
    material.setPhongShininess( fvalue );
    
    
    // Get diffuse texture
    unsigned int numDiffuseTex = aiMat.GetTextureCount( aiTextureType_DIFFUSE );
    if (numDiffuseTex != 0u)
    {      
      aer::Texture *pTexture = SetupTexture( dirname, aiMat, aiTextureType_DIFFUSE, 0u );
      material.setDiffuseMap( pTexture );
    }    
  }
    
  importer.FreeScene();
}


void Model::render(aer::Camera *camera)
{
  m_program.bind();
  
  m_program.setUniform( "uModelViewProjMatrix", camera->getViewProjectionMatrix() );//
  
  for (aer::U32 i=0; i<m_objects.size(); ++i)
  {
    Object_t &obj = m_objects[i];
    
    const aer::Material &material = m_materials[ obj.id_mat ];
    
    if (material.hasDiffuseMap()) {
      m_program.setUniform( "uDiffuseMap", 0);
      material.getDiffuseMap()->bind();
    } else {
      m_program.setUniform( "uDiffuseColor", material.getPhongDiffuse());
    }
    m_program.setUniform( "uEnableTexturing", material.hasDiffuseMap());
    
    
    obj.vao.bind();
      glDrawArrays( GL_TRIANGLES, 0, obj.numVertices);
    obj.vao.unbind();
  }
}










namespace {
  
aer::Texture* SetupTexture( const std::string &dirname, const aiMaterial &aiMat, 
                           aiTextureType type, unsigned int index)
{  
  aiString filename;
  aiTextureMapMode mapmode[3];
  
  // retrieve the texture properties
  aiMat.GetTexture( type, index, &filename, NULL, NULL, NULL, NULL, mapmode);
  std::string image_loc = dirname + filename.data;
  
  /// Create a texture object and load an image into it
  aer::Texture *texture = Application::sTextureProxy.get( image_loc );;
  
  texture->bind();
  {
    //texture->setFilters( aer::Texture::LINEAR, aer::Texture::LINEAR );
    
    aer::Texture::WrapMode mode;
    mode = (mapmode[0] == aiTextureMapMode_Wrap)? aer::Texture::REPEAT
                                                : aer::Texture::CLAMP_TO_BORDER;
    texture->setWrap( aer::Texture::TEXTURE_WRAP_S, mode);
    
    mode = (mapmode[1] == aiTextureMapMode_Wrap)? aer::Texture::REPEAT
                                                : aer::Texture::CLAMP_TO_BORDER;
    texture->setWrap( aer::Texture::TEXTURE_WRAP_T, mode);
    
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 4);//
    glGenerateMipmap( GL_TEXTURE_2D );
  }
  aer::Texture::Unbind( aer::Texture::TEXTURE_2D, 0u);
  
  return texture;
}

}
